/*
 * Copyright (C) 2002 Roman Zippel <zippel@linux-m68k.org>
 * Released under the terms of the GNU GPL v2.0.
 */

#include <locale.h>
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <errno.h>

#include "lkc.h"

static void conf(struct menu* menu);
static void check_conf(struct menu* menu);
static void xfgets(char* str, int size, FILE* in);

enum input_mode {
	oldaskconfig,
	silentoldconfig,
	oldconfig,
	allnoconfig,
	allyesconfig,
	allmodconfig,
	alldefconfig,
	randconfig,
	defconfig,
	savedefconfig,
	listnewconfig,
	olddefconfig,
} input_mode = oldaskconfig;

static int indent = 1;
static int tty_stdio;
static int valid_stdin = 1;
static int sync_kconfig;
static int conf_cnt;
static char line[PATH_MAX];
static struct menu* rootEntry;

static void print_help(struct menu* menu)
{
	struct gstr help = str_new();

	menu_get_ext_help(menu, &help);

	printf("\n%s\n", str_get(&help));
	str_free(&help);
}

static void strip(char* str)
{
	char* p = str;
	int l;

	while((isspace(*p)))
		p++;

	l = strlen(p);

	if(p != str)
		memmove(str, p, l + 1);

	if(!l)
		return;

	p = str + l - 1;

	while((isspace(*p)))
		*p-- = 0;
}

static void check_stdin(void)
{
	if(!valid_stdin) {
		printf(_("aborted!\n\n"));
		printf(_("Console input/output is redirected. "));
		printf(_("Run 'make oldconfig' to update configuration.\n\n"));
		exit(1);
	}
}

static int conf_askvalue(struct symbol* sym, const char* def)
{
	enum symbol_type type = sym_get_type(sym);

	if(!sym_has_value(sym))
		printf(_("(NEW) "));

	line[0] = '\n';
	line[1] = 0;

	if(!sym_is_changable(sym)) {
		printf("%s\n", def);
		line[0] = '\n';
		line[1] = 0;
		return 0;
	}

	switch(input_mode) {
		case oldconfig:
		case silentoldconfig:
			if(sym_has_value(sym)) {
				printf("%s\n", def);
				return 0;
			}

			check_stdin();

		/* fall through */
		case oldaskconfig:
			fflush(stdout);
			xfgets(line, sizeof(line), stdin);

			if(!tty_stdio)
				printf("\n");

			return 1;

		default:
			break;
	}

	switch(type) {
		case S_INT:
		case S_HEX:
		case S_STRING:
			printf("%s\n", def);
			return 1;

		default:
			;
	}

	printf("%s", line);
	return 1;
}

static int conf_string(struct menu* menu)
{
	struct symbol* sym = menu->sym;
	const char* def;

	while(1) {
		printf("%*s%s ", indent - 1, "", _(menu->prompt->text));
		printf("(%s) ", sym->name);
		def = sym_get_string_value(sym);

		if(sym_get_string_value(sym))
			printf("[%s] ", def);

		if(!conf_askvalue(sym, def))
			return 0;

		switch(line[0]) {
			case '\n':
				break;

			case '?':

				/* print help */
				if(line[1] == '\n') {
					print_help(menu);
					def = NULL;
					break;
				}

			/* fall through */
			default:
				line[strlen(line) - 1] = 0;
				def = line;
		}

		if(def && sym_set_string_value(sym, def))
			return 0;
	}
}

static int conf_sym(struct menu* menu)
{
	struct symbol* sym = menu->sym;
	tristate oldval, newval;

	while(1) {
		printf("%*s%s ", indent - 1, "", _(menu->prompt->text));

		if(sym->name)
			printf("(%s) ", sym->name);

		putchar('[');
		oldval = sym_get_tristate_value(sym);

		switch(oldval) {
			case no:
				putchar('N');
				break;

			case mod:
				putchar('M');
				break;

			case yes:
				putchar('Y');
				break;
		}

		if(oldval != no && sym_tristate_within_range(sym, no))
			printf("/n");

		if(oldval != mod && sym_tristate_within_range(sym, mod))
			printf("/m");

		if(oldval != yes && sym_tristate_within_range(sym, yes))
			printf("/y");

		if(menu_has_help(menu))
			printf("/?");

		printf("] ");

		if(!conf_askvalue(sym, sym_get_string_value(sym)))
			return 0;

		strip(line);

		switch(line[0]) {
			case 'n':
			case 'N':
				newval = no;

				if(!line[1] || !strcmp(&line[1], "o"))
					break;

				continue;

			case 'm':
			case 'M':
				newval = mod;

				if(!line[1])
					break;

				continue;

			case 'y':
			case 'Y':
				newval = yes;

				if(!line[1] || !strcmp(&line[1], "es"))
					break;

				continue;

			case 0:
				newval = oldval;
				break;

			case '?':
				goto help;

			default:
				continue;
		}

		if(sym_set_tristate_value(sym, newval))
			return 0;

help:
		print_help(menu);
	}
}

static int conf_choice(struct menu* menu)
{
	struct symbol* sym, *def_sym;
	struct menu* child;
	bool is_new;

	sym = menu->sym;
	is_new = !sym_has_value(sym);

	if(sym_is_changable(sym)) {
		conf_sym(menu);
		sym_calc_value(sym);

		switch(sym_get_tristate_value(sym)) {
			case no:
				return 1;

			case mod:
				return 0;

			case yes:
				break;
		}
	} else {
		switch(sym_get_tristate_value(sym)) {
			case no:
				return 1;

			case mod:
				printf("%*s%s\n", indent - 1, "", _(menu_get_prompt(menu)));
				return 0;

			case yes:
				break;
		}
	}

	while(1) {
		int cnt, def;

		printf("%*s%s\n", indent - 1, "", _(menu_get_prompt(menu)));
		def_sym = sym_get_choice_value(sym);
		cnt = def = 0;
		line[0] = 0;

		for(child = menu->list; child; child = child->next) {
			if(!menu_is_visible(child))
				continue;

			if(!child->sym) {
				printf("%*c %s\n", indent, '*', _(menu_get_prompt(child)));
				continue;
			}

			cnt++;

			if(child->sym == def_sym) {
				def = cnt;
				printf("%*c", indent, '>');
			} else
				printf("%*c", indent, ' ');

			printf(" %d. %s", cnt, _(menu_get_prompt(child)));

			if(child->sym->name)
				printf(" (%s)", child->sym->name);

			if(!sym_has_value(child->sym))
				printf(_(" (NEW)"));

			printf("\n");
		}

		printf(_("%*schoice"), indent - 1, "");

		if(cnt == 1) {
			printf("[1]: 1\n");
			goto conf_childs;
		}

		printf("[1-%d", cnt);

		if(menu_has_help(menu))
			printf("?");

		printf("]: ");

		switch(input_mode) {
			case oldconfig:
			case silentoldconfig:
				if(!is_new) {
					cnt = def;
					printf("%d\n", cnt);
					break;
				}

				check_stdin();

			/* fall through */
			case oldaskconfig:
				fflush(stdout);
				xfgets(line, sizeof(line), stdin);
				strip(line);

				if(line[0] == '?') {
					print_help(menu);
					continue;
				}

				if(!line[0])
					cnt = def;
				else if(isdigit(line[0]))
					cnt = atoi(line);
				else
					continue;

				break;

			default:
				break;
		}

conf_childs:

		for(child = menu->list; child; child = child->next) {
			if(!child->sym || !menu_is_visible(child))
				continue;

			if(!--cnt)
				break;
		}

		if(!child)
			continue;

		if(line[0] && line[strlen(line) - 1] == '?') {
			print_help(child);
			continue;
		}

		sym_set_choice_value(sym, child->sym);

		for(child = child->list; child; child = child->next) {
			indent += 2;
			conf(child);
			indent -= 2;
		}

		return 1;
	}
}

static void conf(struct menu* menu)
{
	struct symbol* sym;
	struct property* prop;
	struct menu* child;

	if(!menu_is_visible(menu))
		return;

	sym = menu->sym;
	prop = menu->prompt;

	if(prop) {
		const char* prompt;

		switch(prop->type) {
			case P_MENU:
				if((input_mode == silentoldconfig ||
				        input_mode == listnewconfig ||
				        input_mode == olddefconfig) &&
				        rootEntry != menu) {
					check_conf(menu);
					return;
				}

			/* fall through */
			case P_COMMENT:
				prompt = menu_get_prompt(menu);

				if(prompt)
					printf("%*c\n%*c %s\n%*c\n",
					       indent, '*',
					       indent, '*', _(prompt),
					       indent, '*');

			default:
				;
		}
	}

	if(!sym)
		goto conf_childs;

	if(sym_is_choice(sym)) {
		conf_choice(menu);

		if(sym->curr.tri != mod)
			return;

		goto conf_childs;
	}

	switch(sym->type) {
		case S_INT:
		case S_HEX:
		case S_STRING:
			conf_string(menu);
			break;

		default:
			conf_sym(menu);
			break;
	}

conf_childs:

	if(sym)
		indent += 2;

	for(child = menu->list; child; child = child->next)
		conf(child);

	if(sym)
		indent -= 2;
}

static void check_conf(struct menu* menu)
{
	struct symbol* sym;
	struct menu* child;

	if(!menu_is_visible(menu))
		return;

	sym = menu->sym;

	if(sym && !sym_has_value(sym)) {
		if(sym_is_changable(sym) ||
		        (sym_is_choice(sym) && sym_get_tristate_value(sym) == yes)) {
			if(input_mode == listnewconfig) {
				if(sym->name && !sym_is_choice_value(sym)) {
					printf("%s%s\n", CONFIG_, sym->name);
				}
			} else if(input_mode != olddefconfig) {
				if(!conf_cnt++)
					printf(_("*\n* Restart config...\n*\n"));

				rootEntry = menu_get_parent_menu(menu);
				conf(rootEntry);
			}
		}
	}

	for(child = menu->list; child; child = child->next)
		check_conf(child);
}

static struct option long_opts[] = {
	{"oldaskconfig",    no_argument,       NULL, oldaskconfig},
	{"oldconfig",       no_argument,       NULL, oldconfig},
	{"silentoldconfig", no_argument,       NULL, silentoldconfig},
	{"defconfig",       optional_argument, NULL, defconfig},
	{"savedefconfig",   required_argument, NULL, savedefconfig},
	{"allnoconfig",     no_argument,       NULL, allnoconfig},
	{"allyesconfig",    no_argument,       NULL, allyesconfig},
	{"allmodconfig",    no_argument,       NULL, allmodconfig},
	{"alldefconfig",    no_argument,       NULL, alldefconfig},
	{"randconfig",      no_argument,       NULL, randconfig},
	{"listnewconfig",   no_argument,       NULL, listnewconfig},
	{"olddefconfig",    no_argument,       NULL, olddefconfig},
	/*
	 * oldnoconfig is an alias of olddefconfig, because people already
	 * are dependent on its behavior(sets new symbols to their default
	 * value but not 'n') with the counter-intuitive name.
	 */
	{"oldnoconfig",     no_argument,       NULL, olddefconfig},
	{NULL, 0, NULL, 0}
};

static void conf_usage(const char* progname)
{

	printf("Usage: %s [-s] [option] <kconfig-file>\n", progname);
	printf("[option] is _one_ of the following:\n");
	printf("  --listnewconfig         List new options\n");
	printf("  --oldaskconfig          Start a new configuration using a line-oriented program\n");
	printf("  --oldconfig             Update a configuration using a provided .config as base\n");
	printf("  --silentoldconfig       Same as oldconfig, but quietly, additionally update deps\n");
	printf("  --olddefconfig          Same as silentoldconfig but sets new symbols to their default value\n");
	printf("  --oldnoconfig           An alias of olddefconfig\n");
	printf("  --defconfig <file>      New config with default defined in <file>\n");
	printf("  --savedefconfig <file>  Save the minimal current configuration to <file>\n");
	printf("  --allnoconfig           New config where all options are answered with no\n");
	printf("  --allyesconfig          New config where all options are answered with yes\n");
	printf("  --allmodconfig          New config where all options are answered with mod\n");
	printf("  --alldefconfig          New config with all symbols set to default\n");
	printf("  --randconfig            New config with random answer to all options\n");
}

int main(int ac, char** av)
{
	const char* progname = av[0];
	int opt;
	const char* name, *defconfig_file = NULL /* gcc uninit */;
	struct stat tmpstat;

	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);

	tty_stdio = isatty(0) && isatty(1) && isatty(2);

	while((opt = getopt_long(ac, av, "s", long_opts, NULL)) != -1) {
		if(opt == 's') {
			conf_set_message_callback(NULL);
			continue;
		}

		input_mode = (enum input_mode)opt;

		switch(opt) {
			case silentoldconfig:
				sync_kconfig = 1;
				break;

			case defconfig:
			case savedefconfig:
				defconfig_file = optarg;
				break;

			case randconfig: {
				struct timeval now;
				unsigned int seed;
				char* seed_env;

				/*
				 * Use microseconds derived seed,
				 * compensate for systems where it may be zero
				 */
				gettimeofday(&now, NULL);
				seed = (unsigned int)((now.tv_sec + 1) * (now.tv_usec + 1));

				seed_env = getenv("KCONFIG_SEED");

				if(seed_env && *seed_env) {
					char* endp;
					int tmp = (int)strtol(seed_env, &endp, 0);

					if(*endp == '\0') {
						seed = tmp;
					}
				}

				fprintf(stderr, "KCONFIG_SEED=0x%X\n", seed);
				srand(seed);
				break;
			}

			case oldaskconfig:
			case oldconfig:
			case allnoconfig:
			case allyesconfig:
			case allmodconfig:
			case alldefconfig:
			case listnewconfig:
			case olddefconfig:
				break;

			case '?':
				conf_usage(progname);
				exit(1);
				break;
		}
	}

	if(ac == optind) {
		printf(_("%s: Kconfig file missing\n"), av[0]);
		conf_usage(progname);
		exit(1);
	}

	name = av[optind];
	conf_parse(name);

	//zconfdump(stdout);
	if(sync_kconfig) {
		name = conf_get_configname();

		if(stat(name, &tmpstat)) {
			fprintf(stderr, _("***\n"
			                  "*** Configuration file \"%s\" not found!\n"
			                  "***\n"
			                  "*** Please run some configurator (e.g. \"make oldconfig\" or\n"
			                  "*** \"make menuconfig\" or \"make xconfig\").\n"
			                  "***\n"), name);
			exit(1);
		}
	}

	switch(input_mode) {
		case defconfig:
			if(!defconfig_file)
				defconfig_file = conf_get_default_confname();

			if(conf_read(defconfig_file)) {
				printf(_("***\n"
				         "*** Can't find default configuration \"%s\"!\n"
				         "***\n"), defconfig_file);
				exit(1);
			}

			break;

		case savedefconfig:
		case silentoldconfig:
		case oldaskconfig:
		case oldconfig:
		case listnewconfig:
		case olddefconfig:
			conf_read(NULL);
			break;

		case allnoconfig:
		case allyesconfig:
		case allmodconfig:
		case alldefconfig:
		case randconfig:
			name = getenv("KCONFIG_ALLCONFIG");

			if(!name)
				break;

			if((strcmp(name, "") != 0) && (strcmp(name, "1") != 0)) {
				if(conf_read_simple(name, S_DEF_USER)) {
					fprintf(stderr,
					        _("*** Can't read seed configuration \"%s\"!\n"),
					        name);
					exit(1);
				}

				break;
			}

			switch(input_mode) {
				case allnoconfig:
					name = "allno.config";
					break;

				case allyesconfig:
					name = "allyes.config";
					break;

				case allmodconfig:
					name = "allmod.config";
					break;

				case alldefconfig:
					name = "alldef.config";
					break;

				case randconfig:
					name = "allrandom.config";
					break;

				default:
					break;
			}

			if(conf_read_simple(name, S_DEF_USER) &&
			        conf_read_simple("all.config", S_DEF_USER)) {
				fprintf(stderr,
				        _("*** KCONFIG_ALLCONFIG set, but no \"%s\" or \"all.config\" file found\n"),
				        name);
				exit(1);
			}

			break;

		default:
			break;
	}

	if(sync_kconfig) {
		if(conf_get_changed()) {
			name = getenv("KCONFIG_NOSILENTUPDATE");

			if(name && *name) {
				fprintf(stderr,
				        _("\n*** The configuration requires explicit update.\n\n"));
				return 1;
			}
		}

		valid_stdin = tty_stdio;
	}

	switch(input_mode) {
		case allnoconfig:
			conf_set_all_new_symbols(def_no);
			break;

		case allyesconfig:
			conf_set_all_new_symbols(def_yes);
			break;

		case allmodconfig:
			conf_set_all_new_symbols(def_mod);
			break;

		case alldefconfig:
			conf_set_all_new_symbols(def_default);
			break;

		case randconfig:

			/* Really nothing to do in this loop */
			while(conf_set_all_new_symbols(def_random)) ;

			break;

		case defconfig:
			conf_set_all_new_symbols(def_default);
			break;

		case savedefconfig:
			break;

		case oldaskconfig:
			rootEntry = &rootmenu;
			conf(&rootmenu);
			input_mode = silentoldconfig;

		/* fall through */
		case oldconfig:
		case listnewconfig:
		case olddefconfig:
		case silentoldconfig:

			/* Update until a loop caused no more changes */
			do {
				conf_cnt = 0;
				check_conf(&rootmenu);
			} while(conf_cnt &&

			        (input_mode != listnewconfig &&
			         input_mode != olddefconfig));

			break;
	}

	if(sync_kconfig) {
		/* silentoldconfig is used during the build so we shall update autoconf.
		 * All other commands are only used to generate a config.
		 */
		if(conf_get_changed() && conf_write(NULL)) {
			fprintf(stderr, _("\n*** Error during writing of the configuration.\n\n"));
			exit(1);
		}

		if(conf_write_autoconf()) {
			fprintf(stderr, _("\n*** Error during update of the configuration.\n\n"));
			return 1;
		}
	} else if(input_mode == savedefconfig) {
		if(conf_write_defconfig(defconfig_file)) {
			fprintf(stderr, _("n*** Error while saving defconfig to: %s\n\n"),
			        defconfig_file);
			return 1;
		}
	} else if(input_mode != listnewconfig) {
		if(conf_write(NULL)) {
			fprintf(stderr, _("\n*** Error during writing of the configuration.\n\n"));
			exit(1);
		}
	}

	return 0;
}

/*
 * Helper function to facilitate fgets() by Jean Sacren.
 */
void xfgets(char* str, int size, FILE* in)
{
	if(fgets(str, size, in) == NULL)
		fprintf(stderr, "\nError in reading or end of file.\n");
}
